/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package cz.hamacekj.geometr;

import cz.hamacekj.geometr.plugin.ConstructionObject;
import cz.hamacekj.geometr.plugin.ICircleObject;
import cz.hamacekj.geometr.plugin.ILineObject;
import cz.hamacekj.geometr.plugin.IPoint;
import cz.hamacekj.geometr.plugin.IPointObject;
import cz.hamacekj.geometr.plugin.IRayObject;
import cz.hamacekj.geometr.plugin.ISegmentObject;
import cz.hamacekj.geometr.plugin.IToolBox;
import cz.hamacekj.geometr.plugin.PrimitiveObject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

/**
 * Implementace {@link IToolBox} nástroje pro operace s objekty.
 * @author hamacekh
 */
public class ToolBox implements IToolBox {
    
    
    private List<PrimitiveObject> intersect2(PrimitiveObject fst, PrimitiveObject snd){
        LinkedList<PrimitiveObject> res = new LinkedList<>();
        Type pfst = primitiveType(fst);
        Type psnd = primitiveType(snd);
        if(fst instanceof IPointObject){
            IPointObject f = (IPointObject)fst;
            if(elem(f.getPoint(), snd)){
                res.add(f);
                return res;
            }else{
                return res;
            }
        }
        if(snd instanceof IPointObject){
            assert pfst != Type.Point;
            IPointObject f = (IPointObject)snd;
            if(elem(f.getPoint(), fst)){
                res.add(f);
                return res;
            }else{
                return res;
            }
        }
        // v tuhle chvíli ani jeden z objektů není bod
        assert !pfst.isPoint();
        assert !psnd.isPoint();
        if(pfst.isLinear() && psnd.isLinear()){
            // najit prunik, zjistit jestli lezi na obou, pripadne ho vratit.
            IPoint X = linearA(fst);
            IPoint Y = linearA(snd);
            double u1 = linearB(fst).getX() - X.getX();
            double u2 = linearB(fst).getY() - X.getY();
            double v1 = linearB(snd).getX() - Y.getX();
            double v2 = linearB(snd).getY() - Y.getY();
            // pokud jsou rovnoběžné, nemají průnik
            double denum = u1 * v2 - u2 * v1;
            if(eq(denum, 0)){
                // "primky" jsou rovnobezne
                return res;
            }
            // tj nejsou rovnobezne
            // spocitam t tak, ze X + t u = prusecik
            double t = (X.getY() * v1 - X.getX() * v2 + Y.getX() * v2 - Y.getY() * v1) / denum;
            double intx = X.getX() + u1 * t;
            double inty = X.getY() + u2 * t;
            IPoint intersection = createPoint(intx, inty);
            if(elem(intersection, fst) && elem(intersection, snd)){
                res.add(createPointObject(intersection));
                return res;
            }else{
                // nastane pokud byl nalezeny prunik, ale nelezi na jedne z primek
                // napr dve mimobezne poloprimky, ktere se neprotinaji.
                return res;
            }
        }
        // alespon jeden z objektu je kruznice
        assert pfst.isCircle() || psnd.isCircle();
        if(pfst.isLinear() || psnd.isLinear()){
            ICircleObject circle;
            PrimitiveObject linear;
            if(pfst.isLinear()){
                assert psnd.isCircle();
                linear = fst;
                circle = (ICircleObject)snd;
            }else{
                assert psnd.isLinear();
                linear = snd;
                circle = (ICircleObject)fst;
            }
            return intersectLC(circle, linear);
        }
        
        assert pfst.isCircle() && psnd.isCircle();
        // hledam prunik dvou kruznic
        ICircleObject circ1 = (ICircleObject)fst;
        ICircleObject circ2 = (ICircleObject)snd;
        // budu hledat chordalu a prevedu tak na problem pruniku primky a kruznice.
        // smerovy vektor chordaly je kolmy na S2 - S1
        double vectx = circ2.getCenter().getX() - circ1.getCenter().getX();
        double vecty = circ2.getCenter().getY() - circ1.getCenter().getY();
        double chordx = vecty;
        double chordy = -vectx;
        // ted potrebuju libovolny bod chordaly. Treba ten na spojnici stredu kruznic.
        double dist = distance(circ2.getCenter(), circ1.getCenter());
        // takze vzdalenost bodu od stredu circ2 je:
        double dr2 = (square(dist) - square(circ1.getRadius()) + square(circ2.getRadius())) / (2*dist);
        // hledany bod je tedy S2 + vect*dr2 kde vect2 je jednotkovy vektor z S2 do S1
        double vect2x = -vectx / dist;
        double vect2y = -vecty / dist;
        Point X = new Point(circ2.getCenter().getX() + vect2x*dr2,
                circ2.getCenter().getY() + vect2y*dr2);
        Point Y = new Point(X.getX() + chordx, X.getY() + chordy);
        LineObject chordal = new LineObject(X, Y);
        return intersect2(circ1, chordal);
    }

    @Override
    public final Set<? extends PrimitiveObject> intersect(PrimitiveObject... objects) {
        ArrayList<Set<? extends PrimitiveObject>> sets = new ArrayList<>(objects.length);
        for(int i = 0; i < objects.length; i++){
            HashSet<PrimitiveObject> hs = new HashSet<>();
            hs.add(objects[i]);
            sets.add(hs);
        }
        return intersect(sets);
    }

    @Override
    public Set<? extends PrimitiveObject> intersect(ConstructionObject... objects) {
        ArrayList<Set<? extends PrimitiveObject>> sets = new ArrayList<>(objects.length);
        for(int i = 0; i < objects.length; i++){
            sets.add(objects[i].getPrimitiveObjects());
        }
        return intersect(sets);
    }

    @Override
    public Set<? extends PrimitiveObject> intersect(ArrayList<Set<? extends PrimitiveObject>> objects) {
        if(objects.size() == 1){
            return objects.get(0);
        }
        if(objects.size() == 2){
            Set<PrimitiveObject> res = new HashSet<>();
            for(PrimitiveObject p: objects.get(0)){
                for(PrimitiveObject s: objects.get(1)){
                    res.addAll(intersect2(p,s));
                }
            }
            return res;
        }
        if(objects.size() > 2){
            ArrayList<Set<? extends PrimitiveObject>> sets = new ArrayList<>(objects.size()-1);
            ArrayList<Set<? extends PrimitiveObject>> arg = new ArrayList<>();
            arg.add(objects.get(0));
            arg.add(objects.get(1));
            sets.add(intersect(arg));
            for(int i = 1; i < objects.size() -1; i++){
                sets.add(objects.get(i+1));
            }
            return intersect(sets);
        }
        return null;
    }

    @Override
    public IPoint createPoint(double x, double y) {
        return new Point(x, y);
    }

    @Override
    public IPointObject createPointObject(IPoint p) {
        return new cz.hamacekj.geometr.PointObject(new Point(p.getX(), p.getY()));
    }

    @Override
    public IPointObject createPointObject(double x, double y) {
        return new cz.hamacekj.geometr.PointObject(new Point(x, y));
    }

    @Override
    public ILineObject createLineObject(IPoint A, IPoint B) {
        return new LineObject(new cz.hamacekj.geometr.Point(A.getX(), A.getY())
                , new cz.hamacekj.geometr.Point(B.getX(), B.getY()));
    }

    @Override
    public ISegmentObject createSegmentObject(IPoint A, IPoint B) {
        return new SegmentObject(new cz.hamacekj.geometr.Point(A.getX(), A.getY())
                , new cz.hamacekj.geometr.Point(B.getX(), B.getY()));
    }

    @Override
    public IRayObject createRayObject(IPoint A, IPoint B) {
        return new RayObject(new cz.hamacekj.geometr.Point(A.getX(), A.getY())
                , new cz.hamacekj.geometr.Point(B.getX(), B.getY()));
    }

    @Override
    public ICircleObject createCircleObject(IPoint center, double radius) {
        return new CircleObject(new cz.hamacekj.geometr.Point(center.getX(), center.getY())
                , radius);
    }
    
    /**
     * Vrátí boolean, jestli bod v prvním argumentu leží na objektu z druhého argumentu.
     * @param p Bod.
     * @param snd Objekt.
     * @return true, pokud bod leží na objektu.
     */
    private boolean elem(IPoint p, PrimitiveObject snd){
        Type psnd = primitiveType(snd);
        if(snd instanceof IPointObject){
                IPointObject s = (IPointObject)snd;
                if(eq(p.getX(), p.getX()) &&
                        eq(p.getY(), s.getPoint().getY())){
                    return true;
                }else{
                    return false;
                }
            }
        if(psnd.isLinear()){
            IPoint a = linearA(snd);
            IPoint b = linearB(snd);
            // toto probehne pokud jeden z objektu je bod, druhy je linearni
            // bod je ted v promenne p, krajni body linearniho objektu v promennych a, b
            // typ objektu je v promenne t
            // ted bude nasledovat hledani pruniku bodu s primkou a pripadne vraceni bodu
            double VectX = b.getX() - a.getX();
            double VectY = b.getY() - a.getY();
            double k;
            boolean validk;
            if(!eq(VectX, 0) && !eq(VectY, 0)){
                k = (p.getX() - a.getX()) / VectX;
                double k1 = (p.getY() - a.getY()) / VectY;
                validk = eq(k, k1);
            }else{
                validk = true;
                if(VectX > VectY){
                    k = (p.getX() - a.getX()) / VectX;
                }else{
                    k = (p.getY() - a.getY()) / VectY;
                }
            }
            boolean pointOnObj = false;
            if ( validk){
                switch(psnd){
                    case Line:
                        return true;
                    case Ray:
                        if(k >= 0){
                            return true;
                        }
                        break;
                    case Segment:
                        if(k >=0 && k<= 1){
                            return true;
                        }
                }
            }
            return false;
        }
        if(psnd.isCircle()){
            // pokud je jeden argument bod a druhý kružnice.
            ICircleObject s = (ICircleObject) snd;
            double radius = s.getRadius();
            IPoint center = s.getCenter();
            if(eq(distance(center, p), radius)){
                return true;
            }
            return false;
        }
        assert false;
        return false;
    }
    
    /**
     * Vrátí průnik lineárního objektu (ray, line, segment) a kružnice.
     * @param circ Kružnice k průniku.
     * @param line Lineární objekt k průniku. Musí jít o lineární typ. Jinak nečekané chování
     * (nejspíš null pointer exception).
     * @return Průnik circ a line.
     */
    private LinkedList<PrimitiveObject> intersectLC(ICircleObject circ, PrimitiveObject line){
        LinkedList<PrimitiveObject> res = new LinkedList<>();
        IPoint center = circ.getCenter();
        double radius = circ.getRadius();
        IPoint X = linearA(line);
        IPoint Y = linearB(line);
        double u1 = Y.getX() - X.getX();
        double u2 = Y.getY() - X.getY();
        
        double xs1 = X.getX() - center.getX();
        double xs2 = X.getY() - center.getY();
        
        double b = 2*(u1 * xs1 + u2 * xs2);
        double a = square(u1) + square(u2);
        double c = square(xs1) + square(xs2) - square(radius);
        double diskrim = square(b) - 4*a*c;
        
        // hledam t tak, ze P = X + t u kde P je prusecik
        if(eq(diskrim, 0)){
            // pokud je to tecna
            double t = - (1/2.0)*(b / a);
            IPoint p = createPoint(X.getX() + t * u1, X.getY() + t*u2);
            if(elem(p, line)){
                res.add(createPointObject(p));
                return res;
            }
            return res;
        }
        if(diskrim < 0){
            // pokud nemaji spolecny bod
            return res;
        }
        assert diskrim > 0;
        // vim, ze existuji dve reseni
        double t1 = (-b + Math.sqrt(diskrim))/ (2*a);
        double t2 = (-b - Math.sqrt(diskrim))/ (2*a);
        IPoint p1 = createPoint(X.getX() + t1 * u1, X.getY() +t1* u2);
        IPoint p2 = createPoint(X.getX() + t2 * u1, X.getY() +t2* u2);
        if(elem(p1, line)){
            res.add(createPointObject(p1));
        }
        if(elem(p2, line)){
            res.add(createPointObject(p2));
        }
        return res;
    }

    @Override
    public ConstructionObject createConstructionObject(
            final String label,
            final boolean isShown,
            final Set<PrimitiveObject> primitiveObjects,
            final String stepDesc) {
        return new ConstructionObject() {

            @Override
            public String getLabel() {
                return label;
            }

            @Override
            public boolean isLabelShown() {
                return isShown;
            }

            @Override
            public Set<PrimitiveObject> getPrimitiveObjects() {
                return primitiveObjects;
            }

            @Override
            public String getStepDescription() {
                return stepDesc;
            }
        };
    }
    
    
    /**
     * Vrátí vzdálenost dvou bodů v rovině.
     * @param a První bod.
     * @param b Druhý bod.
     * @return Jejich vzdálenost.
     */
    @Override
    public double distance(IPoint a, IPoint b){
        double vectX = b.getX() - a.getX();
        double vectY = b.getY() - a.getY();
        return Math.sqrt(vectX*vectX + vectY*vectY);
    }
    
    // ZAKLADNI NASTROJE PRO PRACI S BODY
    
    private double square(double x){
        return x*x;
    }
    
    
    /**
     * Vrátí jestli jsou dvě čísla v argumentech přibližně stejná.
     * Budu jí používat k porovnávání všech bodů.
     * @param a První číslo k porovnání.
     * @param b Druhé číslo k porovnání.
     * @return boolean jestli jsou stejná.
     */
    private boolean eq(double a, double b){
        return Math.abs(a - b) < 0.0001;
    }
    
    private IPoint linearA(PrimitiveObject p){
        if(p instanceof ILineObject){
            return ((ILineObject)p).getA();
        }
        if(p instanceof ISegmentObject){
            return ((ISegmentObject)p).getA();
        }
        if(p instanceof IRayObject){
            return ((IRayObject)p).getA();
        }
        return null;
    }
    
    private IPoint linearB(PrimitiveObject p){
        if(p instanceof ILineObject){
            return ((ILineObject)p).getB();
        }
        if(p instanceof ISegmentObject){
            return ((ISegmentObject)p).getB();
        }
        if(p instanceof IRayObject){
            return ((IRayObject)p).getB();
        }
        return null;
    }
    
    private Type primitiveType(PrimitiveObject p){
        if(p instanceof IPointObject){
            return Type.Point;
        }
        if(p instanceof ILineObject){
            return Type.Line;
        }
        if(p instanceof ISegmentObject){
            return Type.Segment;
        }
        if(p instanceof IRayObject){
            return Type.Ray;
        }
        if(p instanceof ICircleObject){
            return Type.Circle;
        }
        assert false;
        return null;
    }
    
    private enum Type {
        
        Point, Segment, Line, Ray, Circle;

        private boolean isLinear() {
            return this == Line || this == Ray || this == Segment;
        }
        private boolean isPoint (){
            return this == Point;
        }
        private boolean isCircle(){
            return this == Circle;
        }
    };
    
}
